/*
 * Limited Rate Streaming Source
 * Copyright (C) 2009 Stefan Dimitrov
 * http://www.chaudcast.com/
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * See COPYING.TXT for details.
 */


package com.chaudcast;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.media.Control;
import javax.microedition.media.protocol.ContentDescriptor;
import javax.microedition.media.protocol.DataSource;
import javax.microedition.media.protocol.SourceStream;

import net.rim.device.api.ui.UiApplication;
import net.rim.device.api.ui.component.TextField;


/**
 * The data source used by the BufferedPlayback's media player.
 */
public final class LimitedRateStreamingSource extends DataSource
{
    
	/** The max size to be read from the stream at one time */
    private static final int READ_CHUNK = 4000; // bytes
    
    /** A reference to the field which displays the load status */
    private TextField _loadStatusField;

    /** A reference to the field which displays the player status */
    private TextField _playStatusField;

    /** Stream remote address */
    private String _host;
    private int _port;
    private String _uri;
    
    /**
     * The minimum number of bytes that must be buffered before the media file
     * will begin playing.
     */
    private int _startBuffer = 256000;

    /** The maximum size (in bytes) of a single read */
    private int _readLimit = 64000;

    
    /**
     * The minimum forward byte buffer which must be maintained in order for
     * the audio to keep playing. If the forward buffer falls below this
     * number, the playback will pause until the buffer increases.
     */
    private int _pauseBytes = 64000;

    /**
     * The minimum forward byte buffer required to resume
     * playback after a pause.
     */
    private int _resumeBytes = 128000;

    /** The stream connection over which media content is passed */
    private StreamConnection _conn;

    /** The stream over which requests are sent to the server */
    private OutputStream _outStream;

    /** The stream over which data is received from the server */
    private InputStream _inStream;
    
    /** A byte array used to store ShoutCast header */
    private byte[] _scIn = new byte[256];
    
    /** A String to store ShoutCast header*/
    private String _scHeader;
    
    /** A flag used to indicate that the header was read*/
    private boolean _scReady=false;
    
    /** The media buffer */
    private CircularByteBuffer _buffer = new CircularByteBuffer(_startBuffer);

    /** A stream to the buffered resource */
    private LimitedRateSourceStream _feedToPlayer;

    /** The MIME type of the remote media file */
    private String _forcedContentType;

    /** A counter for the total number of buffered bytes */
    private volatile int _totalRead;

    /** A flag used to tell the connection thread to stop */
    private volatile boolean _stop = true;

    /**
     * A flag used to indicate that the initial buffering is complete. In
     * other words, that the current buffer is larger than the defined start
     * buffer size.
     */
    private volatile boolean _bufferingComplete;

    /** The thread which retrieves the remote media file */
    private ConnectionThread _loaderThread;


    /**
     * Constructor
     * @param locator The locator that describes the DataSource.
     */
    LimitedRateStreamingSource(String locator)
    {
        super(locator);
        _host = Util.getHost(locator);
        _port = Util.getPort(locator);
        _uri = Util.getUri(locator);
        /*try {
        	Network.open(locator);
        } catch (IOException ioe) {
        	System.out.println("Exception: "+ioe);
        }*/
    }

    /**
     * Open a connection to the locator
     * @throws IOException Thrown if the firewall disallows a connection
     */
    public void connect() throws IOException
    {
        // Open the connection to the remote stream
    	_conn = (StreamConnection)Connector.open("socket://"+_host+":"+_port+";deviceside=true");           
    	_outStream = _conn.openOutputStream();
        //_outStream.write((new String("GET "+_uri+" HTTP/1.1\n\n")).getBytes());
        _inStream = _conn.openInputStream();
		
    	
        // read ShoutCast header
        _inStream.read(_scIn);
        while (!_scReady) {
        	_scHeader = new String(_scIn);
        	if (_scHeader.indexOf("Cache-Control: no-cache")==-1 && _scHeader.indexOf("icy-br:")==-1) {
        			System.out.println("Received: " + (_scHeader));
        			_inStream.read(_scIn);
        	} else {
        			_scReady = true;
        	}
        }

        if (_forcedContentType != null)
        {
            // Use the user-defined content type if it is set
            _feedToPlayer = new LimitedRateSourceStream(_inStream, _forcedContentType);
        }
        else
        {
            // Otherwise, assume mp3
            _feedToPlayer = new LimitedRateSourceStream(_inStream, "audio/mpeg");
        }
    }

    /**
     * Destroy and close all existing connections
     */
    public void disconnect() {
        try 
        {
            if (_inStream != null)
            {
                // Close the reader stream
            	_inStream.close();
            	_inStream = null;   
            }
            
            if (_outStream != null)
            {
            	// Close the writer stream
            	_outStream.close();
            	_outStream = null;
            }
            
            // Close the remote file connection
            if (_conn != null)
            {
            	_conn.close();
            }

            // Close the stream to the player
            if (_feedToPlayer != null)
            {
            	_feedToPlayer.close();
            }
        }
        catch (Exception e)
        {
            ChaudCast.errorDialog(e.toString());
        }
    }

    /**
     * Returns the content type of the remote file
     * @return The content type of the remote file
     */
    public String getContentType()
    {
        return _feedToPlayer.getContentDescriptor().getContentType();
    }

    /**
     * Returns a stream to the buffered resource
     * @return A stream to the buffered resource
     */
    public SourceStream[] getStreams()
    {
        return new SourceStream[] { _feedToPlayer };
    }

    /**
     * Starts the connection thread used to download the remote file
     */
    public void start() throws IOException
    {
    		_stop = false;
            // Open the connection thread to finish downloading the file
            _loaderThread = new ConnectionThread();
            _loaderThread.start();
    }

    /**
     * Stop the connection thread
     */
    public void stop() throws IOException
    {
        // Set the boolean flag to stop the thread
    	_stop = true;    
    }

    /**
     * @see javax.microedition.media.Controllable#getControl(String)
     */
    public Control getControl(String controlType)
    {
        // No implemented Controls
        return null;
    }

    /**
     * @see javax.microedition.media.Controllable#getControls()
     */
    public Control[] getControls()
    {
        // No implemented Controls
        return null;
    }

    /**
     * Force the lower level stream to a given content type. Must be called
     * before the connect function in order to work.
     * @param contentType The content type to use.
     */
    void setContentType(String contentType)
    {
        _forcedContentType = contentType;
    }

    /**
     * A stream to the buffered media resource
     */
    private final class LimitedRateSourceStream implements SourceStream
    {
   	
    	/** Describes the content type of the media file */
        private ContentDescriptor _contentDescriptor;

        /**
         * Constructor. Creates a LimitedRateSourceStream from
         * the given InputStream.
         * @param inputStream The input stream used to create a new reader.
         * @param contentType The content type of the remote file.
         */
        LimitedRateSourceStream(InputStream inputStream, String contentType)
        {
            _inStream = inputStream;
            _contentDescriptor = new ContentDescriptor(contentType);
        }

        /**
         * Returns the content descriptor for this stream
         * @return The content descriptor for this stream
         */
        public ContentDescriptor getContentDescriptor()
        {
            return _contentDescriptor;
        }

        /**
         * Returns -1
         * @return long -1
         */
        public long getContentLength()
        {
            return -1;
        }

        /**
         * Returns the seek type of the stream: NOT_SEEKABLE
         */
        public int getSeekType()
        {
            return NOT_SEEKABLE;
        }

        /**
         * Returns the maximum size (in bytes) of a single read
         */
        public int getTransferSize() 
        {
            return _readLimit;
        }

        /**
         * Writes bytes from the buffer into a byte array for playback
         * @param bytes The buffer into which the data is read
         * @param off The start offset in array b at which the data is written
         * @param len The maximum number of bytes to read
         * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the stream has been reached.
         * @throws IOException Thrown if a read error occurs
         */
        public int read(byte[] bytes, int off, int len) throws IOException
        {
        	int result = 0;
        	int available =_buffer.getAvailable();
        	
        	// ensure we are not asking for too much
        	if (len > getReadLimit()) len = getReadLimit();
        	
        	
        	// A boolean flag indicating that the thread should pause
            // until the buffer has increased sufficiently.
            boolean paused = false;

            while (!_stop)
            {
                available = _buffer.getAvailable();

                if(_bufferingComplete)
                {
                    if (paused && available > getResumeBytes())
                    {
                        //If the video is paused due to buffering, but the
                        //number of available byes is sufficiently high,
                        //resume playback of the media.
                        System.out.println("Resuming - Available: " + available);
                        updatePlayStatus("Resuming " + available + " Bytes");
                        paused = false;
                        return result=_buffer.getInputStream().read(bytes, off, len);
                    }
                    else if(!paused && (available > getPauseBytes() || available > len))
                    {
                        // We have enough information for this media playback

                        if (available < getPauseBytes())
                        {
                            // If the buffer is now insufficient, set the
                            // pause flag.
                            paused = true;
                            updatePlayStatus("Pausing " + available + " Bytes");
                        }

                        System.out.println("Reading: " + len + " - Available: " + available);
                        return result=_buffer.getInputStream().read(bytes, off, len);
                    }
                    else if(!paused)
                    {
                        // Set pause until loaded enough to resume
                        paused = true;
                        updatePlayStatus("Pausing " + available + " Bytes");
                    }
                }
                else
                {
                    // We are not ready to start yet, try sleeping to allow the
                    // buffer to increase.
                    try
                    {
                        Thread.sleep(500);
                    }
                    catch (Exception e)
                    {
                        ChaudCast.errorDialog("Thread.sleep(long) threw " + e.toString());
                    }
                }
            }
            return result;
        }


        /**
         * Close the stream
         * @throws IOException Thrown if the stream could not be closed
         */
        void close() throws IOException
        {
            if (_inStream != null) 
            {
            	_inStream.close();
            }
        }

        /**
         * @see javax.microedition.media.Controllable#getControl(String)
         */
        public Control getControl(String controlType)
        {
            // No implemented controls
            return null;
        }

        /**
         * @see javax.microedition.media.Controllable#getControls()
         */
        public Control[] getControls()
        {
            // No implemented controls
            return null;
        }

		public long seek(long arg0) throws IOException {
			// TODO Auto-generated method stub
			return 0;
		}

		public long tell() {
			// TODO Auto-generated method stub
			return 0;
		}
    }

    /**
     * A thread which downloads the remote file and writes it to the local file
     */
    private final class ConnectionThread extends Thread
    {    	
        /**
         * Fill the buffer
         * @see java.lang.Thread#run()
         */
        public void run()
        {
        		try
            	{
        	    	byte[] temp = new byte[READ_CHUNK];
        	    	int len = 0;
        	    	updateLoadStatus("Buffering...");
        	    	_buffer.clear();
        	    	
        			while (!_stop && (-1 != (len=_inStream.read(temp)))) {
        				_totalRead += len;
        				 updateLoadStatus(_totalRead + " Bytes");
        				_buffer.getOutputStream().write(temp);
       				 	if (!_bufferingComplete && _totalRead > getStartBuffer())
       				 	{
       				 		// We have enough of a buffer to begin playback
       				 		_bufferingComplete = true;
       				 		System.out.println("Initial Buffering Complete");
       				 		updateLoadStatus("Buffering Complete");
       				 	}
        			} // close while
        			
        			if (_stop) {
        				System.out.println("Stopping.");
        				return;
        			}
            	}
            	catch (Exception e)
            	{
            		ChaudCast.errorDialog(e.toString());
            	}
        }
    }

    /**
     * Gets the minimum forward byte buffer which must be maintained in
     * order for the video to keep playing.
     * @return The pause byte buffer.
     */
    int getPauseBytes()
    {
        return _pauseBytes;
    }

    /**
     * Sets the minimum forward buffer which must be maintained in order
     * for the video to keep playing.
     * @param pauseBytes The new pause byte buffer
     */
    void setPauseBytes(int pauseBytes)
    {
        _pauseBytes = pauseBytes;
    }

    /**
     * Gets the maximum size (in bytes) of a single read
     * @return The maximum size (in bytes) of a single read
     */ 
    int getReadLimit()
    {
        return _readLimit;
    }

    /**
     * Sets the maximum size (in bytes) of a single read
     * @param readLimit The new maximum size (in bytes) of a single read
     */
    void setReadLimit(int readLimit)
    {
        _readLimit = readLimit;
    }

    /**
     * Gets the minimum forward byte buffer required to resume
     * playback after a pause.
     * @return The resume byte buffer
     */
    int getResumeBytes()
    {
        return _resumeBytes;
    }

    /**
     * Sets the minimum forward byte buffer required to resume
     * playback after a pause.
     * @param resumeBytes The new resume byte buffer
     */
    void setResumeBytes(int resumeBytes)
    {
        _resumeBytes = resumeBytes;
    }

    /**
     * Gets the minimum number of bytes that must be buffered before the
     * media file will begin playing.
     * @return The start byte buffer
     */
    int getStartBuffer()
    {
        return _startBuffer;
    }

    /**
     * Sets the minimum number of bytes that must be buffered before the
     * media file will begin playing.
     * @param startBuffer The new start byte buffer
     */
    void setStartBuffer(int startBuffer)
    {
        _startBuffer = startBuffer;
    }

    /**
     * Gets a reference to the text field where load status updates
     * are written.
     * @return The load status text field
     */
    TextField getLoadStatus()
    {
        return _loadStatusField;
    }

    /**
     * Sets a reference to the text field where load status updates
     * are written.
     * @param loadStatus The new load status text field
     */
    void setLoadStatus(TextField loadStatus)
    {
        _loadStatusField = loadStatus;
    }

    /**
     * Gets a reference to the text field where player status
     * updates are written.
     * @return The player status text field
     */
    TextField getPlayStatus()
    {
        return _playStatusField;
    }

    /**
     * Sets a reference to the text field where player status
     * updates are written.
     * @param playStatus The new player status text field
     */
    void setPlayStatus(TextField playStatus)
    {
        _playStatusField = playStatus;
    }

    /**
     * Update the player status field
     * @param text The new message to be displayed
     */
    void updatePlayStatus(String text)
    {
        updateStatus(getPlayStatus(), text);
    }

    /**
     * Update the load status field
     * @param text The new message to be displayed
     */
    void updateLoadStatus(String text)
    {
        updateStatus(getLoadStatus(), text);
    }

    /**
     * Update a given status field
     * @param field The field to be updated
     * @param text The message to be displayed in the field
     */
    void updateStatus(TextField field, String text)
    {
        synchronized (UiApplication.getEventLock())
        {
            field.setText(text);
        }
    }
}